package tau.freedbimporter;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import org.apache.tools.bzip2.CBZip2InputStream;
//import org.xeustechnologies.jtar.TarEntry;
import org.xeustechnologies.jtar.TarEntry;
import org.xeustechnologies.jtar.TarInputStream;

import tau.pimpmyfreedb.common.IProgressUpdateClient;

public class FreeDBPackParser {    
    private TarInputStream tarInputStream;
    private FileInputStream fileInputStream;
    private CBZip2InputStream bz2InputStream;

    public FreeDBPackParser(String tarBZ2FilePath)
	    throws FileNotFoundException, IOException {
	createStream(tarBZ2FilePath);
    }

    public boolean readEntries(int numberOfEntriesToRead,
	    ArrayList<RawFreeDBDiscInfo> result,
	    IProgressUpdateClient progressUpdateClient) throws IOException {
	result.clear();

	// Start iterating through the entries
	int readEntryCount = 0;
	TarEntry tarEntry = null;
	while (readEntryCount < numberOfEntriesToRead
		&& (tarEntry = tarInputStream.getNextEntry()) != null
		&& !progressUpdateClient.isCancelled()) {
	    try {
		RawFreeDBDiscInfo di = parseFreeDBEntry();
		if (di != null) {
		    result.add(di);
		}
	    } catch (IOException exception) {
		// In case some items weren't read -- it's ok. There are enough
		// items for everyone.
	    }
	    readEntryCount++;
	}

	return tarEntry != null;
    }

    private void createStream(String tarBZ2FilePath)
	    throws FileNotFoundException, IOException {
	File file = new File(tarBZ2FilePath);
	fileInputStream = new FileInputStream(file);

	if (fileInputStream.read() != 'B' || fileInputStream.read() != 'Z') {
	    // TODO: error
	}
	// This should be created after reading the first two chars.
	bz2InputStream = new CBZip2InputStream(fileInputStream);
	// To read the underlying .tar file
	tarInputStream = new TarInputStream(bz2InputStream);
    }

    private RawFreeDBDiscInfo parseFreeDBEntry() throws IOException {
	// Read an entry using a Reader.
	BufferedReader reader = new BufferedReader(new InputStreamReader(
		tarInputStream));

	// An xmcd file has to start with "# xmcd".
	// Validate that this is a legal file:
	String firstLine = reader.readLine();
	if (firstLine == null || !firstLine.startsWith("# xmcd")) {
	    return null;
	}

	RawFreeDBDiscInfo di = new RawFreeDBDiscInfo();
	// The comment area contains info such as:
	// Track offsets and Disc length.
	try {
	    String line = parseCommentArea(reader, di);

	    // Advance past the comment area to the properties area
	    line = findLineDoesntStartWith(reader, line, "#");
	    parseProperties(reader, di, line);
	} catch (NotASCIIException e) {
	    // If not all English, don't add disc.
	    // System.out.println("Skipping " + e.getNotAsciiText());
	    di = null;
	} catch (NumberFormatException e) {
	    // If not all English, don't add disc.
	    // System.out.println("Skipping " + e.getNotAsciiText());
	    di = null;
	}

	return di;
    }

    private String parseCommentArea(BufferedReader reader, RawFreeDBDiscInfo di)
	    throws IOException {
	String line = "";
	line = findLineStartsWith(reader, line, "# Track frame offsets:");

	Vector<Integer> trackOffsets = new Vector<Integer>();

	while ((line = reader.readLine()) != null) {
	    // Expecting: "#	{track offset}"
	    if (!line.startsWith("#"))
		break;

	    String number = line.substring(1).trim();
	    if (number.length() == 0 || !isNumeric(number))
		break;

	    trackOffsets.add(Integer.parseInt(line.substring(1).trim()));
	}
	di.setTrackOffsets(trackOffsets);
	line = findLineStartsWith(reader, line, "# Disc length: ");
	String[] parts = line.split(" ");
	try {
	    int i = 3;
	    while (i < parts.length && parts[i].length() == 0)
		i++;
	    di.setDiscLength(Integer.parseInt(parts[i]));
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
	di.initTrackLengths();
	return line;
    }

    private void parseProperties(BufferedReader reader, RawFreeDBDiscInfo di,
	    String line) throws IOException, NotASCIIException {
	HashMap<String, String> discProperties = new HashMap<String, String>();
	while (line != null) {
	    // Expecting lines in the format: KEY=value
	    if (!line.contains("="))
		continue;

	    parsePropertyLine(line, discProperties);
	    line = reader.readLine();
	}

	for (Map.Entry<String, String> property : discProperties.entrySet()) {
	    parseProperty(property.getKey(), property.getValue(), di);
	}
    }

    private void parsePropertyLine(String line,
	    HashMap<String, String> discProperties) throws NotASCIIException {
	String[] lineParts = line.split("=");
	String key = lineParts[0];
	String value = (lineParts.length < 2) ? "" : lineParts[1];

	if (!isPureAscii(value)) {
	    throw new NotASCIIException(value);
	}

	// \n and \t in the input should be considered as newlines/tabs.
	value = value.replace("\\n", "\n").replace("\\t", "\t");
	if (!discProperties.containsKey(key)) {
	    discProperties.put(key, "");
	}

	// Support more than one line of same key: append value to
	// existing
	// value.
	// For example, these two lines:
	// TTITLE15=Rudolf Sieczynski: Wien, Wien, nur du allein
	// (Vienna,
	// City of
	// TTITLE15= My Dreams) (Tracy Dahi, soprano)
	// will result with a single key TITLE15, with those two lines
	// concatenated
	// as the value.
	String existing = discProperties.get(key);
	discProperties.put(lineParts[0], existing + value);
    }

    private void parseProperty(String key, String value, RawFreeDBDiscInfo di) {
	if (key.equals("DISCID")) {
	    di.setDiscID(value);
	} else if (key.equals("DTITLE")) {
	    di.setTitle(value);
	} else if (key.equals("DYEAR")) {
	    if (value.length() > 0)
		di.setYear(Integer.parseInt(value));
	} else if (key.equals("DGENRE")) {
	    di.setGenre(value);
	} else if (key.startsWith("TTITLE")) {
	    int trackNumber = Integer
		    .parseInt(key.substring("TTITLE".length()));
	    di.setTrackTitle(trackNumber, value);
	} else if (key.equals("EXTD")) {
	    di.setExtendedData(value);
	} else if (key.startsWith("EXTT")) {
	    int trackNumber = Integer.parseInt(key.substring("EXTT".length()));
	    di.setExtendedTrackData(trackNumber, value);
	} else {
	    di.setAdditionalData(key, value);
	}
    }

    private String findLineStartsWith(BufferedReader reader, String line,
	    String prefix) throws IOException {
	while (!line.startsWith(prefix) && (line = reader.readLine()) != null) {
	}
	return line;
    }

    private String findLineDoesntStartWith(BufferedReader reader, String line,
	    String prefix) throws IOException {
	while (line.startsWith(prefix) && (line = reader.readLine()) != null) {
	}
	return line;
    }

    private static boolean isNumeric(String inputData) {
	return inputData.matches("[-+]?\\d+(\\.\\d+)?");
    }

    public void close() throws IOException {
	tarInputStream.close();
	bz2InputStream.close();
	fileInputStream.close();
    }

    static CharsetEncoder asciiEncoder = Charset.forName("US-ASCII")
	    .newEncoder(); // or "ISO-8859-1" for ISO Latin 1

    public static boolean isPureAscii(String v) {
	return asciiEncoder.canEncode(v);
    }

    private class NotASCIIException extends Exception {
	private static final long serialVersionUID = 757080677934631153L;
	private String notAsciiText;

	public NotASCIIException(String notAsciiText) {
	    this.notAsciiText = notAsciiText;
	}

	@SuppressWarnings("unused")
	public String getNotAsciiText() {
	    return notAsciiText;
	}

    }

}
